package config

import (
	"bytes"
	"fmt"
	"strings"

	"emperror.dev/errors"
	"github.com/spf13/viper"
	"gopkg.in/yaml.v2"
)

// Config root config
type Config struct {
	Server ServerConfig `mapstructure:"server"`
	DB     DBConfig     `mapstructure:"db"`
}

// ServerConfig basic configs for server
type ServerConfig struct {
	Name         string   `mapstructure:"name"`
	Port         int      `yaml:"port,omitempty"`
	AllowOrigins []string `mapstructure:"alloworigins"`
	Debug        bool     `mapstructure:"debug"`
	Secret       string   `mapstructure:"secret"`
	RandomSecret bool     `mapstructure:"randomsecret"`
}

// DBConfig used to configure database
type DBConfig struct {
	SQL   DBConfigItem `mapstructure:"sql"`
	Redis DBConfigItem `mapstructure:"redis"`
}

// DBConfigItem used to configure database
type DBConfigItem struct {
	DBType string `mapstructure:"type" yaml:"type"`
	URL    string `mapstructure:"url"`
}

// validate validates the config
func (cfg *Config) validate() error {
	err := cfg.Server.validate()
	if err != nil {
		return err
	}
	err = cfg.DB.validate()
	return err
}

func (cfg *ServerConfig) validate() error {
	if cfg.Port <= 0 || cfg.Port > 65535 {
		return fmt.Errorf("cfg.Port %d out of range", cfg.Port)
	}
	if cfg.Secret == "" {
		return fmt.Errorf("cfg.Secret should not be empty")
	}
	return nil
}

func (cfg *DBConfig) validate() error {
	err := cfg.SQL.validate()
	if err != nil {
		return errors.Wrap(err, "Sql")
	}
	err = cfg.Redis.validate()
	return errors.Wrap(err, "Redis")
}

func (cfg *DBConfigItem) validate() error {
	if cfg.DBType == "" {
		return errors.New("DBType must not be empty")
	}
	return nil
}

// NewCfg new config with default value
func NewCfg() *Config {
	return &Config{
		Server: ServerConfig{
			Name:         "happy-sudoku",
			Port:         9090,
			AllowOrigins: []string{"http://localhost:8080"},
			// !!Warning, You should specify it by env or config file
			// !! OR it will use this default secret
			Secret:       "CHANGE_IT_IN_PRODUCTION",
			RandomSecret: false,
			Debug:        false,
		},
		DB: DBConfig{
			SQL: DBConfigItem{
				DBType: "sqlite3",
				URL:    "db.sqlite",
			},
			Redis: DBConfigItem{
				DBType: "miniredis",
				URL:    "",
			},
		},
	}
}

// ParseConfig will parse config by settings
func ParseConfig() (*Config, error) {
	const errHeader = "ParseConfig"

	v := viper.New()
	b, err := yaml.Marshal(NewCfg())
	if err != nil {
		return nil, err
	}
	defaultConfig := bytes.NewReader(b)
	v.SetConfigType("yaml")
	if err := v.MergeConfig(defaultConfig); err != nil {
		return nil, err
	}
	v.SetConfigName("config")
	v.AddConfigPath(".")
	v.AddConfigPath("./configs")

	if err := v.MergeInConfig(); err != nil {
		if _, ok := err.(viper.ConfigParseError); ok {
			return nil, err
		}
		// dont return error if file is missing. overwrite file is optional
	}
	// tell viper to overrwire env variables
	v.AutomaticEnv()
	v.SetEnvPrefix("HS")
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

	// refresh configuration with all merged values
	config := &Config{}
	err = v.Unmarshal(config)
	if err != nil {
		return nil, err
	}
	// return config, config.validate()
	return config, nil
}

// Marshal Configs
func (cfg *Config) Marshal() (string, error) {
	out, err := yaml.Marshal(*cfg)
	if err != nil {
		return "", err
	}
	return string(out), nil
}
